////////////////////////////////////////////////////////////////////////////////
// Includes
////////////////////////////////////////////////////////////////////////////////

#include "SCEngine.hpp"

#include "SCSystem.hpp"
#include "SCProgram.hpp"
#include "SComputationInfo.hpp"

#include <cstdlib>
#include <ctime>
#include <cmath>
#include <set>

#include "Exception.hpp"
#include "SizeException.hpp"
#include "IOException.hpp"
#include "OutOfBoundsException.hpp"

#include "SCFuncPtrWrapper.hpp"

#include "Convert.hpp"
#include "Diff.hpp"

////////////////////////////////////////////////////////////////////////////////
// Namespaces
////////////////////////////////////////////////////////////////////////////////

namespace SC {

////////////////////////////////////////////////////////////////////////////////
// Implementation
////////////////////////////////////////////////////////////////////////////////

using namespace std;
using namespace Toolkit;
using namespace Exceptions;

SCEngine::SCEngine()
: recording_level(0), max_nb_memorised_states(0)
{
	// Initialises values
	this->Clear();
	// Initialises random number generatyor
	srand(time(0)+clock());
}

////////////////////////////////////////////////////////////////////////////////
// Public methods
////////////////////////////////////////////////////////////////////////////////

const bool SCEngine::Iterate() {

	// Handle past states buffer
	if (this->max_nb_memorised_states > 0) this->memorise_state();

	// --- Increment attempted iterations ---
	++this->nb_iterations;

	// Check the priority contexts list
	if (this->pr_contexts.empty())
		this->pr_contexts = this->data.Contexts();

	// --- 1: Pick up a context ---
	if (this->pr_contexts.size() == 0) {
		// No computation occurred
		this->last_computation.has_computed = false;
		// No context were picked
		this->last_computation.ctx_idx = -1;
		// No computation occurred
		return false;	
	}
	unsigned int idx = rand()%this->pr_contexts.size();
	SCSystem & context = *this->pr_contexts[idx];

	// Remove it from the priority list
	this->pr_contexts.erase(this->pr_contexts.begin()+idx);

	// --- 2: Create a lists of eligible subjects within a scope ---
	vector<SCSystem *> eligible1, eligible2;
	vector<SCSystem *> scopes = context.super_systems;
	this->last_computation.scope_idx = -1;
	while ((this->last_computation.scope_idx == -1) && (scopes.size() > 0)) {
		eligible1.clear();
		eligible2.clear();
		SCSystem & super = *scopes[rand()%scopes.size()];
		// Check that the system is not the context
		if (&super != &context) {
			// If the definition matches, add to the list of potential subjects
			if (this->match(context.GetSchema1(),super.GetDefinition()))
				eligible1.push_back(&super);
			if (this->match(context.GetSchema2(),super.GetDefinition()))
				eligible2.push_back(&super);
		}
		for (unsigned int j=0; j<super.NbSubSystems(); ++j) {
			SCSystem & sub = *super.sub_systems[j];
			// Check that the system is not the context
			if (&sub == &context) continue;
			// If the definition matches, add to the list of potential subjects
			if (this->match(context.GetSchema1(),sub.GetDefinition()))
				if (find(eligible1.begin(),eligible1.end(),&sub) == eligible1.end())
					eligible1.push_back(&sub);
			if (this->match(context.GetSchema2(),sub.GetDefinition()))
				if (find(eligible2.begin(),eligible2.end(),&sub) == eligible2.end())
					eligible2.push_back(&sub);
		}
		// If eligible 2 contains only one element, this element should be
		// removed from eligible 1 as it cannot be used by both and it could not
		// be paired if it was used as first system.
		if (eligible2.size() == 1) {
			vector<SCSystem *>::iterator itor;
			if ((itor = find(eligible1.begin(),eligible1.end(),eligible2[0])) != eligible1.end())
				eligible1.erase(itor);
		}
		// If a list is empty, no interation is possible within the current scope
		if ( eligible1.empty() || eligible2.empty() )
			scopes.erase(find(scopes.begin(),scopes.end(),&super));
		else
			this->last_computation.scope_idx = (int)(&super - &this->data.GetSystems().front());
	}

	// If eligible systems were found
	if (this->last_computation.scope_idx != -1) {
	
		// --- 3: Pick up the first subject in the first eligible subjects list ---
		SCSystem & sys1 = *eligible1[rand()%eligible1.size()];

		// --- 4: Remove the first system from the list of second eligible ones if part of it ---
		vector<SCSystem *>::iterator itor;
		if ((itor = find(eligible2.begin(),eligible2.end(),&sys1)) != eligible2.end())
			eligible2.erase(itor);
		
		// If enough eligible systems were found to attempt a computation
		if (!eligible2.empty()) {

			// --- 5: Pick up the second subject in the second eligible subjects list ---
			SCSystem & sys2 = *eligible2[rand()%eligible2.size()];

			// --- 6: Record if sys1 or sys2 are contexts before computation
			bool cs1 = (find(this->data.GetContexts().begin(), this->data.GetContexts().end(), &sys1) != this->data.GetContexts().end()),
				 cs2 = (find(this->data.GetContexts().begin(), this->data.GetContexts().end(), &sys2) != this->data.GetContexts().end());

			// --- 7: If recording level is at least 1, backup systems
			SCSystem sys1_sav, ctx_sav, sys2_sav;
			if (this->recording_level == 2) {
				sys1_sav = sys1;
				ctx_sav = context;
				sys2_sav = sys2;
			}

			// --- 8: Apply the systemic computation ---
			if (this->compute(sys1,context,sys2)) {
				
				// Computation occurred
				this->last_computation.has_computed = true;
				
				// --- Increment computations ---
				++this->nb_computations;
				
				// Keep the context list and priority context list up to date
				if (this->data.GetFuncMap().find(context.GetKernel().substr(this->func[0],this->func[1])) == this->data.GetFuncMap().end())
					this->data.RemoveContext(context);
				if (cs1) {
					if (this->data.GetFuncMap().find(sys1.GetKernel().substr(this->func[0],this->func[1])) == this->data.GetFuncMap().end()) {
						this->data.RemoveContext(sys1);
						std::vector<SC::SCSystem *>::iterator itor = find(this->pr_contexts.begin(),this->pr_contexts.end(),&sys1);
						if (itor != this->pr_contexts.end()) this->pr_contexts.erase(itor);
					}
				}
				else if (this->data.GetFuncMap().find(sys1.GetKernel(this->func[0],this->func[1])) != this->data.GetFuncMap().end())
					this->data.AddContext(sys1);
				if (cs2) {
					if (this->data.GetFuncMap().find(sys2.GetKernel(this->func[0],this->func[1])) == this->data.GetFuncMap().end()) {
						this->data.RemoveContext(sys2);
						std::vector<SC::SCSystem *>::iterator itor = find(this->pr_contexts.begin(),this->pr_contexts.end(),&sys2);
						if (itor != this->pr_contexts.end()) this->pr_contexts.erase(itor);
					}
				}
				else if (this->data.GetFuncMap().find(sys2.GetKernel(this->func[0],this->func[1])) != this->data.GetFuncMap().end())
					this->data.AddContext(sys2);
				
				// If recording level is at least 1
				if (this->recording_level >= 1) {
					// Store the index of the last systems involved
					const SCSystem * offset = &this->data.GetSystems().front();
					this->last_computation.sys1_idx = static_cast<unsigned int>(&sys1 - offset);
					this->last_computation.ctx_idx = static_cast<unsigned int>(&context - offset);
					this->last_computation.sys2_idx = static_cast<unsigned int>(&sys2 - offset);
					// If recording level is at 2, compute changes
					if (this->recording_level == 2) {
						this->find_system_changes(sys1_sav,	sys1,	this->last_computation.sys1_changes);
						this->find_system_changes(ctx_sav,	context,this->last_computation.ctx_changes);
						this->find_system_changes(sys2_sav,	sys2,	this->last_computation.sys2_changes);
					}
				}

				// Computation occurred
				return true;
			}
		}
	}
	// No computation occurred
	this->last_computation.has_computed = false;
	// Store the index of the picked context
	if (this->recording_level > 0)
		this->last_computation.ctx_idx = (unsigned int)(&context - &this->data.GetSystems().front());
	// No computation occurred
	return false;
}

const bool SCEngine::Execute(istream & stream) {

	// Handle past states buffer
	if (this->max_nb_memorised_states > 0) this->memorise_state();

	// Read last computation
	if (!this->read_last_computation(stream)) throw IOException("End of log stream");

	// Check the priority contexts list (It needs to be maintained for new
	// computations to run eventually after logged instructions in the same
	// conditions as when these logged instructions were randomly picked).
	if (this->pr_contexts.empty())
		this->pr_contexts = this->data.Contexts();

	// Check indices
	if (this->last_computation.ctx_idx >= (int)this->data.GetNbSystems())
		throw OutOfBoundsException("Incorrect context system index in recorded computation");

	// --- 1: Get the context ---
	SCSystem & context = this->data.System(this->last_computation.ctx_idx);
	
	// Remove it from the priority list
	vector<SCSystem *>::iterator ctx_itor = find(this->pr_contexts.begin(),this->pr_contexts.end(),&context);
	if (ctx_itor == this->pr_contexts.end())
		throw Exception("Incorrect context system in recorded computation (not part of eligible contexts list)");
	this->pr_contexts.erase(ctx_itor);

	// If a computation occurred
	if (this->last_computation.has_computed) {

		// Check indices
		if ((this->last_computation.sys1_idx >= (int)this->data.GetNbSystems()) ||
			(this->last_computation.sys2_idx >= (int)this->data.GetNbSystems()))
			throw OutOfBoundsException("Incorrect system index in recorded computation");

		// --- 2: Get the two systems ---
		SCSystem	& sys1 = this->data.System(this->last_computation.sys1_idx),
					& sys2 = this->data.System(this->last_computation.sys2_idx);	

		// --- 3: Record if sys1 or sys2 are contexts before computation
		bool cs1 = (find(this->data.GetContexts().begin(), this->data.GetContexts().end(), &sys1) != this->data.GetContexts().end()),
			 cs2 = (find(this->data.GetContexts().begin(), this->data.GetContexts().end(), &sys2) != this->data.GetContexts().end());
	
		// --- 4 : Apply the pre-processed systemic computation ---
		this->data.ApplyComputation(this->last_computation);
		// Keep the context list and priority context list up to date
		if (this->data.GetFuncMap().find(context.GetKernel().substr(this->func[0],this->func[1])) == this->data.GetFuncMap().end())
			this->data.RemoveContext(context);
		if (cs1) {
			if (this->data.GetFuncMap().find(sys1.GetKernel().substr(this->func[0],this->func[1])) == this->data.GetFuncMap().end()) {
				this->data.RemoveContext(sys1);
				std::vector<SC::SCSystem *>::iterator itor = find(this->pr_contexts.begin(),this->pr_contexts.end(),&sys1);
				if (itor != this->pr_contexts.end()) this->pr_contexts.erase(itor);
			}
		}
		else if (this->data.GetFuncMap().find(sys1.GetKernel(this->func[0],this->func[1])) != this->data.GetFuncMap().end())
			this->data.AddContext(sys1);
		if (cs2) {
			if (this->data.GetFuncMap().find(sys2.GetKernel(this->func[0],this->func[1])) == this->data.GetFuncMap().end()) {
				this->data.RemoveContext(sys2);
				std::vector<SC::SCSystem *>::iterator itor = find(this->pr_contexts.begin(),this->pr_contexts.end(),&sys2);
				if (itor != this->pr_contexts.end()) this->pr_contexts.erase(itor);
			}
		}
		else if (this->data.GetFuncMap().find(sys2.GetKernel(this->func[0],this->func[1])) != this->data.GetFuncMap().end())
			this->data.AddContext(sys2);

		// Computation occurred
		return true;
	}

	// No computation occurred
	return false;
}

void SCEngine::InitData(const SCProgram & prog, const vector<string> & plugins_dirs) {
	this->data.Init(prog,plugins_dirs);
	this->pr_contexts.clear();
	// Accelerator variables
	this->func[0] = this->data.GetFuncOffset().GetBegin();
	this->func[1] = this->data.GetFuncOffset().GetEnd() - this->data.GetFuncOffset().GetBegin() + 1;
}

void SCEngine::Clear() {
	this->data.Clear();
	this->pr_contexts.clear();
	this->func[0] = this->func[1] = -1;
	this->nb_iterations = this->nb_computations = 0;
	this->memorised_states.clear();
}

const bool SCEngine::IsValidString(const string & str) const {
	if (str.size() != this->data.GetWordLength()) return false;
	for (unsigned int i=0; i<str.size(); ++i)
		if ((str[i] != '0') && (str[i] != '1') && (str[i] != WILDCARD) && (this->data.GetCodeMap().find(str[i]) == this->data.GetCodeMap().end()) )
			return false;
	return true;
}

void SCEngine::WriteLastComputation(ostream & stream) {
	// If a computation occurred
	if (this->last_computation.has_computed) {
		// Computation number
		stream << this->nb_iterations << " " << this->nb_computations << "  ";
		// Index of the scope
		stream << this->last_computation.scope_idx << " ";
		// Index of the three systems
		stream << this->last_computation.sys1_idx << " " << this->last_computation.ctx_idx << " " << this->last_computation.sys2_idx << "  ";
		// Changes on systems
		this->write_system_changes(this->last_computation.sys1_changes, stream); stream << " ";
		this->write_system_changes(this->last_computation.ctx_changes, stream); stream << " ";
		this->write_system_changes(this->last_computation.sys2_changes, stream); stream << endl;
	}
	// No computation occurred at the last iteration
	else
		// Iteration number and picked context index, a computation number of 0
		// indicates that no computation occurred
		stream << this->nb_iterations << " " << -1 << "  " << this->last_computation.ctx_idx << endl;
}

void SCEngine::SetMaxNbMemorisedStates(const unsigned int value) {
	this->max_nb_memorised_states = value;
	while (this->memorised_states.size() > this->max_nb_memorised_states)
		this->memorised_states.pop_front();
}

const bool SCEngine::RestoreState(unsigned int steps_back) {
	if ((steps_back == 0) || (steps_back > this->memorised_states.size())) return false;
	// Remove all states not required
	for (unsigned int i=0; i<steps_back-1; ++i)
		this->memorised_states.pop_back();
	// Get the required state
	SCProgramState & state = this->memorised_states.back();
	// Restore
	this->nb_iterations = state.nb_iterations;
	this->nb_computations = state.nb_computations;
	for (unsigned int i=0; i<this->data.GetNbSystems(); ++i)
		this->data.System(i) = state.systems[i];
	this->data.Contexts() = state.contexts;
	this->pr_contexts = state.pr_contexts;
	this->last_computation = state.last_computation;
	// Remove it
	this->memorised_states.pop_back();
	// Restoration ok
	return true;
}

////////////////////////////////////////////////////////////////////////////////
// Protected methods
////////////////////////////////////////////////////////////////////////////////

const bool SCEngine::match(const string & desc, const string & targ) {
	// Translate description string into its extended form
	string ext;
	map<char,string>::const_iterator itor;
	for (unsigned int i=0; i<desc.size(); ++i) {
		if ((itor = this->data.GetCodeMap().find(desc[i])) ==  this->data.GetCodeMap().end())
			throw Exception("Code mapping failed : cannot find the sequence for the code value '" + desc.substr(i,1) + "'");
		ext += itor->second;
	}
	// Check that the resulting string matches the target one
	if (ext.size() != targ.size())
		throw SizeException("String match failed : uncompressed string's size and target string's size do not match");
	for (unsigned int i=0; i<targ.size(); ++i)
		if ((ext[i] != WILDCARD) && (ext[i] != targ[i]))
			return false;
	return true;
}

const bool SCEngine::compute(
	SCSystem & sys1,
	SCSystem & context,
	SCSystem & sys2
) {
	// Get the pointer associated to the function code
	map<string,SCFuncPtrWrapper>::const_iterator itor;
	if ((itor = this->data.GetFuncMap().find(context.GetKernel(this->func[0],this->func[1]))) ==  this->data.GetFuncMap().end())
		return false;
	// Call the pointed function
	return (*(itor->second.Ptr()))(sys1,context,sys2);
}

void SCEngine::find_system_changes(const SCSystem & old_sys, const SCSystem & new_sys, SCSystemChanges & changes) {
	// Strings
	changes.s1_changes	= DiffStr(old_sys.schema1,	new_sys.schema1);
	changes.k_changes	= DiffStr(old_sys.kernel,	new_sys.kernel);
	changes.s2_changes	= DiffStr(old_sys.schema2,	new_sys.schema2);
	// Super systems changes (keeps the index in the systems list)
	vector< pair<bool, SCSystem *> > tmp_sys = DiffVec<SCSystem *>(old_sys.super_systems,new_sys.super_systems);
	changes.super_changes.clear();
	for (unsigned int i=0; i<tmp_sys.size(); ++i) {
		for (unsigned int s=0; s<this->data.GetNbSystems(); ++s) {
			if (&this->data.GetSystem(s) == tmp_sys[i].second) {
				changes.super_changes.push_back(pair<bool,unsigned int>(tmp_sys[i].first,s));
				break;
			}
		}
	}
	// Sub systems changes (keeps the index in the systems list)
	tmp_sys = DiffVec<SCSystem *>(old_sys.sub_systems,new_sys.sub_systems);
	changes.sub_changes.clear();
	for (unsigned int i=0; i<tmp_sys.size(); ++i) {
		for (unsigned int s=0; s<this->data.GetNbSystems(); ++s) {
			if (&this->data.GetSystem(s) == tmp_sys[i].second) {
				changes.sub_changes.push_back(pair<bool,unsigned int>(tmp_sys[i].first,s));
				break;
			}
		}
	}
}

void SCEngine::write_system_changes(const SCSystemChanges & sys_changes, ostream & stream) {
	// Number of changes on schema 1
	stream << sys_changes.s1_changes.size() << " ";
	// Schema 1 changes (location, change)
	for (unsigned int i=0; i<sys_changes.s1_changes.size(); ++i)
		stream << sys_changes.s1_changes[i].first << " " << sys_changes.s1_changes[i].second << " ";
	// Number of changes on kernel
	stream << sys_changes.k_changes.size() << " ";
	// Kernel changes (location, change)
	for (unsigned int i=0; i<sys_changes.k_changes.size(); ++i)
		stream << sys_changes.k_changes[i].first << " " << sys_changes.k_changes[i].second << " ";
	// Number of changes on schema 2
	stream<< sys_changes.s2_changes.size() << " ";
	// Schema 2 changes (location, change)
	for (unsigned int i=0; i<sys_changes.s2_changes.size(); ++i)
		stream << sys_changes.s2_changes[i].first << " " << sys_changes.s2_changes[i].second << " ";
	// Number of changes on super systems
	stream << sys_changes.super_changes.size() << " ";
	// Super systems changes (location, change)
	for (unsigned int i=0; i<sys_changes.super_changes.size(); ++i)
		stream << sys_changes.super_changes[i].first << " " << sys_changes.super_changes[i].second << " ";
	// Number of changes on sub systems
	stream << sys_changes.sub_changes.size() << " ";
	// Sub systems changes (location, change)
	for (unsigned int i=0; i<sys_changes.sub_changes.size(); ++i)
		stream << sys_changes.sub_changes[i].first << " " << sys_changes.sub_changes[i].second << " ";
}

const bool SCEngine::read_last_computation(istream & stream) {
	string comp_str;
	std::getline(stream, comp_str);
	if (!stream.good()) return false;
	stringstream comp_stream; comp_stream << comp_str;
	int computations;
	comp_stream >> this->nb_iterations >> computations;
	if (computations >= 0) {
		this->nb_computations = static_cast<unsigned int>(computations);
		this->last_computation.has_computed = true;
		comp_stream >> this->last_computation.scope_idx ;
		comp_stream >> this->last_computation.sys1_idx >> this->last_computation.ctx_idx >> this->last_computation.sys2_idx;
		this->read_system_changes(this->last_computation.sys1_changes, comp_stream);
		this->read_system_changes(this->last_computation.ctx_changes, comp_stream);
		this->read_system_changes(this->last_computation.sys2_changes, comp_stream);
	}
	else {
		this->last_computation.has_computed = false;
		comp_stream >> this->last_computation.ctx_idx;
	}
	return true;
}

void SCEngine::read_system_changes(SCSystemChanges & sys_changes, istream & stream) {
	unsigned int size;
	// Number of changes on schema 1
	stream >> size;
	sys_changes.s1_changes.resize(size);
	// Schema 1 changes (location, change)
	for (unsigned int i=0; i<sys_changes.s1_changes.size(); ++i)
		stream >> sys_changes.s1_changes[i].first >> sys_changes.s1_changes[i].second;
	// Number of changes on kernel
	stream >> size;
	sys_changes.k_changes.resize(size);
	// Kernel changes (location, change)
	for (unsigned int i=0; i<sys_changes.k_changes.size(); ++i)
		stream >> sys_changes.k_changes[i].first >> sys_changes.k_changes[i].second;
	// Number of changes on schema 2
	stream >> size;
	sys_changes.s2_changes.resize(size);
	// Schema 2 changes (location, change)
	for (unsigned int i=0; i<sys_changes.s2_changes.size(); ++i)
		stream >> sys_changes.s2_changes[i].first >> sys_changes.s2_changes[i].second;
	// Number of changes on super systems
	stream >> size;
	sys_changes.super_changes.resize(size);
	// Super systems changes (location, change)
	for (unsigned int i=0; i<sys_changes.super_changes.size(); ++i)
		stream >> sys_changes.super_changes[i].first >> sys_changes.super_changes[i].second;
	// Number of changes on sub systems
	stream >> size;
	sys_changes.sub_changes.resize(size);
	// Sub systems changes (location, change)
	for (unsigned int i=0; i<sys_changes.sub_changes.size(); ++i)
		stream >> sys_changes.sub_changes[i].first >> sys_changes.sub_changes[i].second;
}

void SCEngine::memorise_state() {
	if (this->memorised_states.size() >= this->max_nb_memorised_states)
		this->memorised_states.pop_front();
	this->memorised_states.push_back(SCProgramState());
	SCProgramState & state = this->memorised_states.back();
	state.nb_iterations = this->nb_iterations;
	state.nb_computations = this->nb_computations;
	state.systems = this->data.GetSystems();
	state.contexts = this->data.GetContexts();
	state.pr_contexts = this->pr_contexts;
	state.last_computation = this->last_computation;
}

/*
const bool SCEngine::IsValidSchema(const string & str) const {
	if (str.size() != this->data.GetWordLength()) return false;
	if (!this->IsValidKernel(str)) {
		for (unsigned int i=0; i<str.size(); ++i)
			if (this->data.GetCodeMap().find(str[i]) == this->data.GetCodeMap().end())
				return false;
	}
	return true;
}
		
const bool SCEngine::IsValidKernel(const string & str) const {
	if (str.size() != this->data.GetWordLength()) return false;
	for (unsigned int i=0; i<str.size(); ++i)
		if ((str[i] != '0') && (str[i] != '1') && (str[i] != WILDCARD))
			return false;
	return true;
}
*/

////////////////////////////////////////////////////////////////////////////////
// Closing Namespaces
////////////////////////////////////////////////////////////////////////////////

}

////////////////////////////////////////////////////////////////////////////////
